fix(agent,ai): degrade primitive stream increments instead of killing managed turns - #4612
Conversation
4e1ac0a to
4285242
Compare
Yeachan-Heo
left a comment
There was a problem hiding this comment.
Approving the fix-forward head 4285242b40 (maintainer rebase of the contributor's two commits onto current dev 6696988b6d; submitted head 4e1ac0a4e preserved in the PR history).
Verified on the exact new head:
- Defect independently reproduced on pre-patch base
c83ffe3d7f(mock managed stream,fallbackManaged: true):thinking_deltawithdelta: undefined/delta: 1→Managed fallback attempt could not produce a serializable event snapshot (local snapshot bug, not a provider failure)— the claimedlocal_snapshot_failure. bun teston 6 targeted files: 208 pass / 0 fail (managed-attempt-transaction, anthropic-stream-envelope, agent-session-fallback-attempt-transaction, agent-loop, openai-codex-stream, openai-responses-multi-toolcall-stream).bun --cwd=packages/agent run checkandbun --cwd=packages/ai run checkgreen.bun run buildgreen; binary smokegjc/0.14.0.- Rebase integrity: all 5 source/test diffs byte-identical to the submitted head; changelog deltas vs dev are exactly one new Unreleased bullet per package, released 0.14.0 sections untouched.
Fail-closed policy (object-shaped payloads, sanitizer sentinels) preserved; remaining intentional throw sites unchanged.
4285242 to
c3f5e70
Compare
|
Dev advanced to What changed in the re-rebase: #4610 ( Verification on
Requesting fresh authorized exact-head review on — |
c3f5e70 to
2a7a102
Compare
|
Dev advanced to
Fresh authorized exact-head review on — |
|
Status on exact head
Holding for fresh authorized exact-head review on — |
2a7a102 to
c580e87
Compare
|
Reconstruction onto live dev Authorship preserved: both commits by Jerome (laerad777), author date 2026-08-17, messages unchanged. Overlap re-inspection (11 dev commits since 7265a61, incl. merge of #4585): none touch this PR's source/test files. The only sibling edits are two new Policy preserved: primitive/missing Validation on
Canonical digest (27afb73...c580e87, binary/full-index/no-ext-diff): Body holds exactly one truthful — |
probepark
left a comment
There was a problem hiding this comment.
Independent maintainer review — merge blocked.
major — executable content is coerced, not just prose
packages/ai/src/providers/anthropic.ts:2306-2313 and packages/ai/src/providers/openai-codex-responses.ts:1266-1267,1296-1297 coerce malformed increments to "" at the provider edge. That also catches object-shaped tool-argument increments, so a malformed tool-call delta becomes an empty argument string and the turn proceeds — bypassing the snapshot layer's fail-closed policy and potentially publishing or executing a tool call with missing/default arguments.
Degrading a malformed thinking delta is fine. Silently erasing a malformed tool argument is a correctness hazard, and it is exactly the class of bug the managed staging layer exists to catch.
Required:
- keep object/function-shaped tool deltas fail-closed
- add tool-argument regression coverage for both providers
- emit a bounded diagnostic when a primitive anomaly is degraded, so the anomaly stays observable rather than vanishing
otherwise
The primitive thinking-delta degradation is a reasonable and welcome change; the blast radius is just drawn too wide.
f7458c7 to
768e66a
Compare
Yeachan-Heo
left a comment
There was a problem hiding this comment.
Adversarial exact-head review of 768e66a4d883fa4b567d2675b6bd8df00e573b14 (base 2bd7b4a48c, live dev tip — MERGEABLE).
The probepark blocker is resolved correctly. Commit 768e66a4d narrows the blast radius exactly as required:
input_json_delta(anthropic.ts:2343) andresponse.function_call_arguments.delta/response.custom_tool_call_input.delta(codex:1100/1117 viaassertPrimitiveToolArgumentIncrement) now fail the turn closed on object/function-shaped increments — executable tool arguments are never coerced to"". The thrown error carries no payload.- Primitive/missing anomalies (
undefined/null/number/boolean) still degrade to""— prose/thinking only — now with one bounded payload-free diagnostic per delta type per stream (noteDegradedIncrement/noteCodexDegradedIncrement: model, provider, delta type, received typeof; never the payload). - Regression coverage present for both providers: object-shaped, function-shaped, primitive-degrade-with-warning, and thinking-degrade cases (anthropic-toolcall-increment-guard + openai-codex-toolcall-increment-guard, 8 tests).
- Sink layer unchanged and correct: object/sentinel payloads stay fail-closed at
event.delta/event.content/shell.content;*_endfull-content republish intact;signature_deltaappends only strings (now also diagnosed when skipped).
Independently verified on exact head (maintainer lane, sole worktree): bun test 11 files 256 pass / 0 fail; bun --cwd=packages/agent run check + bun --cwd=packages/ai run check green; verify-gjc-state-writers --fail green; changelog-history-guard green (2bd7b4a..HEAD); git diff --check clean; bun run build green with gjc/0.14.0 smoke (dist SHA-256 79e0599a9e5d3a0430f9e5036861d3db59e2141bfea6c2e2decbd2bc90bb4da4). Digest cfe54934… recorded in the body verdict.
Note on process: maintainer approvals on this PR have been dismissed before as stale after head moves; this approval targets the exact current head and stands until the head changes again. Product CI on this head: 19 green / 6 skipped; the two contract gates hold by design on needs-human until an independent exact-head review lands.
—
[repo owner's gaebal-gajae (clawdbot) 🦞]
|
Review feedback addressed on exact head
@probepark @snowykr, please re-review the current exact head when the remaining product job settles. |
|
Re-review requested on exact head @probepark — your CHANGES_REQUESTED@c580e8727 findings are addressed by contributor commit
Maintainer verification on exact head: Body verdict updated to head — |
Verdict: Request changesReviewed the exact current PR head SummaryThe PR correctly normalizes benign primitive text/thinking increments and rejects object-shaped tool arguments in several paths. However, the current implementation treats malformed primitive tool-argument increments as empty strings, and it drops malformed Anthropic signatures. Those cases can produce executable calls with missing arguments or turn private thinking into ordinary text, including unredacted request-dump output. FindingsHigh — primitive tool-argument increments are still silently executable
High — malformed
|
| Axis | Result |
|---|---|
| A1 Intent / Policy / Contract | Request changes — malformed executable increments violate the stated fail-closed contract. |
| A2 Architecture / Correctness / Failure | Request changes — snapshot getter/function paths can erase deltas instead of failing closed. |
| A3 Security / Privacy / Trust | Request changes — tool-argument alteration and private-thinking redaction bypass. |
| A4 Verification / Tests / CI | Clear — provided exact-head evidence and changed tests are materially relevant; live malformed-wire coverage remains a limitation. |
| A5 Context / Compatibility / Platform | Request changes — Codex custom-tool initial input and Anthropic stale-index paths remain unchecked. |
Once malformed tool arguments and malformed private-thinking signatures preserve the fail-closed boundary, the normalization approach looks appropriate.
probepark
left a comment
There was a problem hiding this comment.
Independent maintainer review at exact head 768e66a4 — merge blocked. The object/function half of my prior finding is fixed; the primitive half is still fail-open on executable data, and the new tests make the risk concrete rather than resolving it.
prior finding — partially fixed
Fixed: object- and function-shaped executable deltas now fail closed in both providers, with regression coverage, and provider-edge primitive degradation emits one bounded payload-free shared-logger warning per increment/event type.
Not fixed: there is still no diagnostic at the shared agent snapshot degradation boundary, so the bounded-diagnostic requirement holds only for the two providers you edited.
major 1 — a degraded primitive can assemble into valid-but-wrong arguments
packages/ai/src/providers/anthropic.ts:2341-2352. A primitive input_json_delta.partial_json is replaced with "" and JSON assembly continues.
{"n":1 + 2 (numeric primitive → "") + 3} → {"n":13}
That parses, passes the completeness check, and executes. It is not a crash and not a rejection — it is a silently different tool call. The PR body's claim that an empty increment is side-effect-free holds for prose; it does not hold for a JSON fragment stream where position carries meaning.
Fail closed on every non-string tool-argument fragment. Prose, thinking, and signature anomalies are the only ones safe to degrade.
major 2 — corrupted stream buffer beats canonical terminal input
packages/ai/src/providers/openai-codex-responses.ts:1477-1488. Custom-tool finalization prefers any non-empty streamed partialJson over output_item.done.item.input.
So a primitive custom-tool delta is erased, the surrounding string fragments leave the buffer non-empty, and that corrupted buffer wins over the complete, authoritative terminal input. Codex function calls do the opposite — they finalize from terminal item.arguments. Two paths in the same provider disagreeing about which source is authoritative is a defect independent of the primitive question.
Make terminal input authoritative with mismatch rejection, or fail closed on every non-string executable delta.
major 3 — malformed Codex deltas keep a managed turn alive
packages/ai/src/providers/openai-codex-responses.ts:146-170. Codex counts every recognized delta envelope as semantic progress regardless of payload, so repeated primitive/no-op deltas reset the idle watchdog indefinitely. Memory stays bounded (managed staging compacts superseded deltas at its cap), but the attempt need not terminate.
Anthropic already requires a non-empty string delta for idle progress. Same class of event, two different liveness semantics. Validate the Codex delta payload in the progress predicate and add a test that malformed deltas do not postpone the timeout.
major 4 — the shared boundary degrades silently, and bigint slips through
packages/agent/src/agent-loop.ts:1122-1138 converts primitive deltas to "" with no diagnostic, and the added transaction test explicitly asserts zero diagnostics. Every provider and custom stream other than the two you edited therefore degrades invisibly.
Separately, bigint is sanitized to a decimal string before this check and forwarded rather than degraded — so it does not behave as the documentation says.
Add a bounded payload-free diagnostic at the shared boundary, or keep it fail-closed unless normalization was already diagnosed upstream.
tolerated-shape inventory
Recording this because it should be a deliberate table, not an emergent one:
- Managed shell
content: missing/undefined/null/number/boolean/non-sentinel strings →[]; arrays normalized; objects and sanitizer sentinels fail closed; functions/symbols sanitize to sentinels and fail; bigint → decimal string →[]. - Managed delta/end fields: missing/undefined/null/number/boolean →
""; ordinary strings retained; objects/arrays and sentinel strings rejected; sanitized function/symbol rejected; bigint retained as decimal string. - Anthropic text/thinking: every non-string →
""; signature ignores every non-string; tool arguments reject object/function but erase other non-strings. - Codex reasoning/text/refusal: same; function/custom tool arguments reject object/function but erase other non-strings.
The two "erase" rows are majors 1 and 2.
Managed transaction interaction is fine: a degraded event stages as an ordinary event, the attempt is accepted after a successful terminal envelope, and it consumes no fallback/retry/resample budget. Anthropic malformed primitives correctly do not refresh the semantic-progress deadline; Codex ones do, which is major 3.
nit
packages/ai/test/anthropic-toolcall-increment-guard.test.ts:78 adds ReturnType<typeof streamAnthropic>. Spell the stream type.
coverage
Scope is focused despite the size (production +168/-33, tests +581/-9, changelogs +3) — no unrelated changes.
The object/function fail-closed tests on both providers are genuine pins, as are the sentinel-delta and Anthropic stream-envelope thinking/signature tests. But the primitive-degradation test enshrines the unsafe policy: its chosen fragments stay correct after deletion, so it demonstrates the mechanism without testing the corruption case. The Codex primitive function-call test only reliably pins the warning, since final arguments come from canonical item.arguments.
Missing: a valid-but-wrong primitive Anthropic argument case, a primitive Codex custom-tool case, a mismatch test proving terminal custom-tool input overrides a corrupted buffer, an idle-timeout test for repeated malformed Codex deltas, a shared-boundary diagnostic test, and any bigint coverage.
Reviewed by @probepark — method: detached worktree at 768e66a4, full read of both provider delta paths and the shared managed snapshot boundary, exhaustive tolerated-shape enumeration per field, provider-vs-provider semantics comparison for finalization and idle progress, managed-transaction budget interaction trace, per-test base-vs-head discrimination. Tests not executed.
gajae.pr-review-verdict.v1 merge-blocked sha256:cfe549344a09f8cd89b6fa0049acd723c9b3a147b10bd21d2f333a14bd156c97 reviewer:human reviewer-id:probepark evidence:exact-head-768e66a4-four-majors-primitive-erasure-assembles-valid-but-wrong-executable-arguments
768e66a to
f1cc479
Compare
Yeachan-Heo
left a comment
There was a problem hiding this comment.
Approving exact head f1cc47952a8e8977e348eb3a205d777759db09a5 (maintainer re-rebase of the contributor's three commits onto live dev ceb31349c2; prior head 768e66a4d8 preserved in history; authorship Jerome/laerad777 with original dates intact).
Delta vs prior head is purely the rebase onto ceb31349c2 (#4633 null-arguments + #4655 tool-failure-envelope landed; disjoint from this PR's sites, one CHANGELOG sibling conflict resolved by stacking bullets under Unreleased). Verified on f1cc47952a: bun test 11 files 256 pass / 0 fail (incl. both toolcall-increment-guard suites); agent+ai package checks, verify-gjc-state-writers --fail, changelog-history-guard, git diff --check green; bun run build green gjc/0.14.0.
Policy per probepark's review, implemented in 768e66a4d and re-verified here: object/function tool-argument increments fail the turn closed at both provider edges; only primitive/missing anomalies degrade, each with one bounded payload-free warning per delta type per stream; sink-layer fail-closed semantics (object/sentinel at event.delta/event.content/shell.content) and *_end full republish unchanged.
—
[repo owner's gaebal-gajae (clawdbot) 🦞]
12c20c2 to
19ca51d
Compare
|
Clean reconstruction pushed as head History repair: the prior head Verdict correction: the body previously claimed All four probepark majors are fixed in this head (same content as reviewed, now on reachable history):
Verification on @probepark — requesting your fresh exact-head review of — |
probepark
left a comment
There was a problem hiding this comment.
Re-review at exact head 19ca51d1 — merge blocked, though this is a real advance: three of my four majors are fixed at the provider edge.
fixed
- Anthropic valid-but-wrong assembly — fixed.
anthropic.ts:2333-2344throws before assembly for every non-stringpartial_json, and the{"n":1+ numeric2+3}case is now a genuine pin. That was the finding I cared most about. - Corrupted buffer beating terminal input — fixed for strings.
openai-codex-responses.ts:1437-1468treatsitem.inputas authoritative and rejects a non-empty streamed mismatch, while a matching terminal input succeeds. The two Codex paths now agree on which source wins. - Malformed Codex deltas refreshing idle progress — fixed in source.
:174-181counts only non-empty string*.deltapayloads, matching Anthropic's semantics.
major 1 — the shared boundary still fails open on executable content
packages/agent/src/agent-loop.ts:1142-1159 groups toolcall_delta with prose deltas and converts missing/null/number/boolean to "".
So the exact defect fixed at the Anthropic edge is still reachable through the shared path, for every provider and custom stream other than the two you edited. The fix is currently a per-producer patch where the policy belongs one layer down.
Bigint compounds it: sanitizedDetachedClone (:733-736) converts bigint to a decimal string before field validation, so it bypasses the warning entirely and is forwarded as executable text.
Split toolcall_delta out and fail closed on every originally-non-string value, preserving original-type provenance through sanitization.
major 2 — canonical terminal Codex inputs are not shape-validated
openai-codex-responses.ts:1418-1477. Function finalization uses item.arguments || "{}", so 0, false, null and undefined all become empty executable arguments. Custom finalization rejects only nullish and will accept numbers, booleans, arrays and objects into arguments.input when no streamed buffer exists.
This one is newly load-bearing because of your fix: making terminal input authoritative means its shape is now the thing that must be trusted. Require strings for both terminal fields and pin the non-string cases.
major 3 — the liveness fix has no regression
packages/ai/test/openai-codex-toolcall-increment-guard.test.ts:257. Nothing repeatedly emits a recognized Codex delta envelope with numeric, missing or empty payloads under a short idle timeout. The existing response.in_progress timeout tests are guards — that type was never in CODEX_PROGRESS_EVENT_TYPES, so they pass on 768e66a4 too.
Add a test that fails on the old head and proves malformed deltas cannot postpone termination. Without it the fix is unprotected against the next refactor of the progress predicate.
minor — the shared warning dedupe is process-global
agent-loop.ts:668-685. The comment says per snapshot call and notes providers dedupe per stream, but the module-global set suppresses every later warning for that field for the process lifetime, and undefined degradation is never warned at all. The managed tests only capture the different local-rejection warning, so the diagnostic you added is itself untested. Scope dedupe to a run or transaction.
minor — changelog and body describe the policy you removed
packages/ai/CHANGELOG.md:5-6 and body :12-14,40-49 still say primitive argument and partial_json anomalies degrade to empty strings and that producer object increments are dropped. The code now correctly rejects them. Say that only prose/thinking/signature anomalies degrade and every non-string executable fragment fails closed — that is a better story anyway.
nit
anthropic-toolcall-increment-guard.test.ts:78 still has the ReturnType<>.
managed interaction, for the record
Degraded prose/thinking increments stage as empty events and do not count as semantic progress in either provider. A successful terminal envelope accepts the attempt without consuming fallback, retry or escaped-argument resample budget; without one, the corrected predicates let the watchdog expire and provider retry loops stay bounded. Provider-edge malformed executable deltas now throw before staging.
The residual hole is major 1: a shared toolcall_delta corruption is accepted on a successful terminal turn and consumes no corrective budget, because nothing detects it.
coverage
Genuine pins: Anthropic object/function/primitive/missing rejection and the valid-but-wrong case; Codex function/custom object rejection; Codex valid-but-wrong function arguments; streamed/terminal mismatch rejection; matching terminal success; Anthropic thinking/signature normalization with its per-stream warning.
Guards: managed object/sentinel fail-closed (pre-existing policy), the response.in_progress timeout tests, matching-terminal positive path.
Missing: repeated malformed Codex deltas vs idle timeout; shared primitive toolcall_delta not corrupting assembled arguments; shared bigint failing closed; the shared degradation warning actually being emitted; malformed canonical terminal item.arguments/item.input; Codex prose/reasoning/refusal primitive degradation.
Reviewed by @probepark — method: detached worktree at 19ca51d1, rebuilt the tolerated-shape inventory field by field across managed shell content, managed delta/end, Anthropic text/thinking/signature/tool-arguments and Codex reasoning/text/refusal/function/custom, provider-vs-provider semantics comparison for finalization and idle progress, managed-transaction budget trace, per-test base-vs-head discrimination. Tests not executed.
gajae.pr-review-verdict.v1 merge-blocked sha256:6f7ed78d4ff29e2c26208a873506e357a62460b9584abd1025bbb513bc4659ae reviewer:human reviewer-id:probepark evidence:exact-head-19ca51d1-shared-toolcall-delta-still-fails-open-and-terminal-codex-inputs-unvalidated
db440e7 to
19ca51d
Compare
… managed turns
Z.AI/Codex thinking-first turns were dying on ManagedAttemptSnapshotError
because a missing or numeric thinking_delta is not a string. Primitive
content/delta now become empty values; object and sanitizer-sentinel
shapes stay fail-closed so toolCalls cannot vanish behind a successful
empty turn.
Lore-id: snapshot-delta-primitive
Constraint: never silently drop toolCalls or object-shaped content
Rejected: degrade all non-array content to [] | hides {0:{type:toolCall}}
Confidence: high
Scope-risk: medium
Reversibility: easy
Tested: managed-attempt-transaction + anthropic-stream-envelope + session fallback transaction
Not-tested: live Z.AI/Codex turn after binary rebuild
A numeric or missing signature used to concatenate into thinkingSignature as "1" or "[object Object]". Only string signatures are appended, matching the other increment coercions on this stream. Lore-id: snapshot-signature-delta-guard Constraint: do not invent a signature from a non-string increment Rejected: String(signature) | pollutes replay identity Confidence: high Scope-risk: narrow Reversibility: easy Tested: anthropic-stream-envelope non-string thinking/signature increment Not-tested: live Z.AI signature_delta wire shape
Object- and function-shaped Anthropic input_json_delta and Codex function/custom tool argument increments used to coerce to "" and continue, which could execute with missing or default tool args. Those shapes now fail the turn closed. Primitive thinking/text anomalies still degrade, with one payload-free diagnostic per type. Lore-id: snapshot-toolarg-failclosed Constraint: never silently execute with missing or default tool args Rejected: coerce object increment to empty string | hides live tool args Confidence: high Scope-risk: narrow Reversibility: easy Tested: anthropic-toolcall-increment-guard + openai-codex-toolcall-increment-guard + related provider suites + packages/ai and packages/agent checks Not-tested: live Anthropic/Codex malformed increment wire shapes
19ca51d to
ccd5d0e
Compare
Keep the Yeachan-Heo#4612 follow-up limited to the reproduced end-content, terminal metadata, and terminal-only input findings. Capture managed end content once, bind terminal names to their declarations, and reject oversized terminal-only tool payloads. Constraint: do not add recursive stream hardening or queue/resource machinery Tested: focused agent and Codex suites; agent and AI package checks Confidence: high Scope-risk: narrow Reversibility: easy
ccd5d0e to
01da849
Compare
|
Reconstructed and squashed the review fixes onto current The PR now has four commits: the contributor’s three logical commits with authorship and authored dates preserved, plus one consolidated review-fix commit. The squash changed commit boundaries only: the final tree is byte-identical to the previously verified Scoped fixes since
Exact-head verification:
Prior approvals and reviews are stale after the history rewrite. The PR verdict is now |
Bind streamed values to their validated authority, fail malformed executable increments closed, preserve benign primitive degradation, and cover the exact shared-boundary and terminal-shape review blockers. Constraint: include only probepark-requested review fixes; exclude unrelated end-content, terminal-name, and payload-cap hardening Confidence: high Scope-risk: medium Reversibility: easy
01da849 to
f859a1b
Compare
|
Follow-up: narrowed the exact head to the outstanding Removed the unrelated hardening added in the previous reconstruction:
The final tree now matches the previously verified blocker-resolution candidate, with the review-fix commits consolidated for readability. Exact-head
The previous |
probepark
left a comment
There was a problem hiding this comment.
Re-review at exact head f859a1b6 — approved. Both majors are fixed fail-closed.
major 1 (shared toolcall_delta failing open) — fixed
agent-loop.ts:1192-1216 reads contentIndex and delta once, requires a non-negative integer index, and records whether cloning needed sanitization. The decisive line:
if (directType === "toolcall_delta" && (deltaSnapshot.sanitized || typeof delta !== "string"))
throw new ManagedAttemptSnapshotError("event.delta");undefined, null, number, boolean, object, function, symbol and bigint all reject. The bigint case is the nice detail — it sanitizes to a decimal string, but sanitized === true preserves that provenance and still rejects. And the value is never defaulted to "" for toolcall_delta, which was the coercion I objected to: coercing a malformed executable delta into a default is still failing open.
The thrown snapshot error is logged by warnManagedSnapshotFailure and is not a transport-retry/fallback trigger, so the attempt fails closed and surfaces.
major 2 (unvalidated terminal Codex inputs) — fixed
openai-codex-responses.ts:1477-1503 requires terminal function item.arguments to be a string, parses it, requires a non-null non-array object, verifies the terminal identity matches the active block, and only then commits. :1512-1559 requires custom-tool item.input to be a string and rejects disagreement with both streamed partialJson and input.done; initial custom input is string-checked at :1182-1197. Non-string, malformed, non-object, mismatched and unfinalized terminal states all surface errors instead of empty arguments.
Also fixed: malformed/no-op Codex deltas no longer refresh idle progress (:174-181), degradation diagnostics are run-scoped, and the changelog placement and ReturnType<> nits are gone.
what degradation actually does
Worth stating since the title implies a fallback: with managed fallback, non-array primitive/missing assistant content becomes [], and non-tool prose/thinking/reasoning delta or end primitives become "". Anthropic and Codex handlers erase non-string prose/thinking increments and ignore malformed signature fragments, with one payload-free logger warning per field/event type.
So the capability lost is an anomalous prose/reasoning/signature fragment — executable tool fragments do not degrade, they hard-fail. That is the right split. The user sees no explicit UI error and missing shell content is silent, which is acceptable for cosmetic fragments but worth knowing.
minor — the managed shell copies more than it validates
agent-loop.ts:1080-1101. managedAssistantShell spreads the entire detached stream record into persistent/parent-visible state and accepts arbitrary strings for api, provider and model, so free-form providerPayload, response/control metadata and unknown extension keys survive the boundary — and native payload can later be replayed to the same provider.
Core fields are overwritten and staging is bounded, so this does not reopen the delta coercion. It predates this delta and surfaced only because I widened the audit this round, so I am not blocking on it. But bind identity to config.model and whitelist the replay metadata.
nit
agent-loop.ts:1141-1151: incompleteArgumentsReason accepts any string and casts to a four-value union. Execution stays fail-closed because incompleteArguments blocks the call, but an unknown persisted value produces misleading truncation guidance. Compare explicitly against truncated, malformed, conflicting, ambiguous.
coverage
Genuine pins, and they target the right things:
managed-attempt-transaction.test.ts:1713-1747calls the exact shared snapshot boundary and asserts undefined/null/number/boolean/biginttoolcall_deltaall throw — every one of those was coerced on the prior head.:1748-1762pins one-read accessor handling against check/read laundering.anthropic-toolcall-increment-guard.test.ts:84-146— the{"n":1+2+3}case specifically pins the previous silent{"n":13}corruption.openai-codex-toolcall-increment-guard.test.ts:582-685— terminal function values[undefined, 0, false, null, array, object], malformed/empty JSON, and non-string terminal custom inputs all assert an error. Directly pins major 2.openai-codex-stream.test.ts:89-129,410-428emits recognized delta envelopes carrying42, missing delta and""under a 20 ms idle timeout — the liveness pin that was missing last round.
Caveat: no full managed-attempt integration test proves a hostile toolcall_delta is discarded before all callbacks and session publication. The boundary test is still a real regression pin because the transaction calls that function synchronously before staging.
scope
Production +430/-141, tests +1355/-40, changelogs +3 across 11 files — about 76% of additions are tests. The Codex websocket timeout/queue changes broaden the surface but directly support the malformed-delta liveness guarantee I asked for, so I do not count them as creep.
Reviewed by @probepark — method: detached worktree at f859a1b6, read of the shared snapshot boundary for coercion-versus-rejection on every primitive type including bigint provenance, terminal Codex validation trace for both function and custom-tool paths, full field-by-field trust-boundary inventory classifying each as closed vocabulary, numeric or free-form, per-test base-vs-head discrimination. Tests not executed.
gajae.pr-review-verdict.v1 merge-approved sha256:5b18971bdd151991d5fe0326835472b2214a204808bacdf3415aadbaebb407bd reviewer:human reviewer-id:probepark evidence:exact-head-f859a1b6-toolcall-delta-rejects-all-non-string-including-sanitized-bigint-and-terminal-codex-inputs-validated
What
Managed fallback no longer kills a whole turn when a provider sends a primitive increment instead of the schema we expect.
Two layers:
Snapshot sink (
packages/agent/src/agent-loop.ts)content→ empty array[](side-effect-free empty turn)*_delta/*_end.content→ empty string""[unserializable],[accessor],[truncated],[Circular]) still throw. That is deliberate:{0:{type:"toolCall"}}can hide real tool calls, and degrading it to[]would drop executable work behind a successful empty turn.Provider producers
packages/ai/src/providers/anthropic.ts, including Z.AI) coerce non-stringtext/thinking/partial_jsonincrements to""before emitting*_delta.packages/ai/src/providers/openai-codex-responses.ts) does the same forrawEvent.delta.signature_deltaappends only string signatures, so a numeric or missing value cannot pollutethinkingSignatureas"1"/"[object Object]".Why
This error kept coming back after several snapshot PRs:
errorKind: local_snapshot_failure. User-visible as an empty assistant turn that cannot be retried.Earlier fixes closed payload-class / proxy / bigint producers, then primitive message content. The live deaths were still happening on thinking-first models:
zai/glm-5.3(anthropic-messages)openai-codex/gpt-5.6-terraThose models often emit the first increment as
thinking_delta. Ifdeltaisundefinedor a numeric token count, the snapshot sink used to requiretypeof delta === "string"and throwevent.delta. Fallback then surfaces a sticky local snapshot failure instead of a normal (possibly empty) increment.Primitive content degrade alone could not stop that path. This PR covers the increment, not just the message shell.
Policy (do not “just degrade everything”)
undefined/null/ number / boolean / non-sentinel string[]or""{0:{type:"toolCall"}}, object-shaped delta)Remaining intentional throw sites (
shell.role,event.contentIndex,event.toolcall,event.snapshot, staging / overflow) are unchanged. If a future live failure names one of those stages, that is the next producer to teach — do not blindly degradetoolCall.Producer object increments are coerced to
""at the Anthropic/Codex stream edge instead of being forwarded to the managed fail-closed throw. That is a producer-edge drop of a non-string increment, not a silent drop of an assembledtoolCall.Testing
Unit / package
On this head (
4e1ac0a4e):bun test packages/agent/test/managed-attempt-transaction.test.ts packages/coding-agent/test/agent-session-fallback-attempt-transaction.test.ts packages/ai/test/anthropic-stream-envelope.test.ts— 108 pass / 0 failthinking_deltabecomes""; later string still appendssignature_deltaignored; later"sig_ok"keptbun --cwd=packages/agent run checkbun --cwd=packages/ai run checkbun run build && bun run dev:link:bin && bun run dev:doctor— smoke-test okbun run check— failed on currentdev, unrelated to this PRscripts/telegram-daemon-generation-guard.test.tsopenspackages/coding-agent/test/notifications-topic-registry.test.tsvia a relativeBun.file(...)path. The file exists at the repo root; the test still ENOENT'd from thecheck:sdk-closurecwd. Biome also warned on unused locals inpackages/coding-agent/test/edit-result-persistence-bounding.test.ts(not in this diff). Rust / package declaration checks passed.Live one-shot (does not reproduce the bug)
Rebuilt
dist/gjcafter4e1ac0a4e, then:OCX/gpt-5.6-terra:lowzai/glm-5.3openai-codex/gpt-5.6-terra4e1ac0a4e)pongpongpongc83ffe3d7pongpongpongNo
local_snapshot_failure/managed fallback attempt rejectedin today's log. Those providers did not emit a non-string increment on these short turns, so live one-shot cannot prove the regression.Managed-stream A/B (this is the bug)
Same
fallbackManaged: trueagent, same three injected events, run against both trees:c83ffe3d74e1ac0a4ethinking_deltadelta: undefinedlocal_snapshot_failure— Managed fallback attempt could not produce a serializable event snapshot (local snapshot bug, not a provider failure)""thinking_deltadelta: 1local_snapshot_failure""content: "hello"(already degraded ondev)So: if the provider sends that increment, old code dies with the user-facing error and this PR does not.
Independent architect review of the first commit: PASS_WITH_COMMENTS, no FAIL blocker. The remaining comment (
signature_deltastring guard) is addressed in4e1ac0a4e.GJC verdict
devbun checkpasses (ran; fail is pre-existing telegram fixture path, not this diff)